Borland Online And The Cobb Group Present:


December, 1995 - Vol. 2 No. 12

C++ Language Quick Tips - Obscure C++

Most of the time, you'll want to use C++ programming techniques that are pretty obvious. However, in certain cases, you may want to push the edge a bit and use some of the lesser-known features of C++.

In this article, we'll discuss three such practices. The first two illustrate little-known features that won't make or break your program but that can simplify some of your coding. The third item represents the downside of pushing the edge­­a C++ statement that doesn't do what you might expect.

Virtual operators

Considering how many things C++ allows you to do when you want to extend the language constructs for your applications, it may surprise you to know that C++ won't allow you to create class-specific operator functions that are virtual.

Virtual functions and function over-loading give C++ polymorphism, the ability to specify that different types of objects will respond to a given input in unique ways. Without polymorphism, you have to explicitly call a function to manipulate a given data structure, and you have the responsibility of determining if the function is the correct one to use for that data.

By overloading operators, you can extend the syntax that C++ provides with your own solution-oriented syntax. If you create the functions for these overloaded operators as members of a class, derived classes will inherit them (except for operator=, which derived classes don't inherit).

Unfortunately, you can't make class-member operator functions virtual. This means that if you want to define an operator function as a member of a class, you'll need to redeclare the function in a derived class to change its behavior. Even worse, if you apply an operator to a base class pointer, the program will execute the base class's version of the operator function and ignore the new version you've defined in the derived class.

The solution is to call a virtual function in the base class that does the actual work for the overloaded operator. You'll overload the operator just once, and then you'll override the base class's virtual function in the derived classes.

In addition, you're not restricted to making the operator function as a member function of the base class. You can instead create a global operator function that calls the base class's virtual function.

For example, if you want to overload the standard insertion operator (operator <<) for a given class and its ancestors, you'll create something similar to the code that appears in Figure A.


Figure A - To make an operator behave polymorphically for a class hierarchy, you'll need to create code similar to this.

#include <iostream.h>

struct Streamer
{  virtual void StreamFunc(ostream& os)
   { os << "Streamer - "; } 
};

class Confetti: public Streamer
{  virtual void StreamFunc(ostream& os)
   { os << "Confetti - ";
     Streamer::StreamFunc(os); }
}; 

// overload << for Streamer objects
ostream& operator <<(ostream& os, 
                     Streamer& object)
{
  object.StreamFunc(os);
  return os; 
}

// overload << for pointers to Streamer 
// objects
ostream& operator <<(ostream& os, 
                     Streamer* object)
{
  object->StreamFunc(os);
  return os; 
}

Initialization argument name scope

When you create a constructor for a class, one of the first things you'll probably do is examine which of the class members you should initialize in an initialization list. (The initialization list for a constructor appears after a colon as a comma-separated list of class member or base class initialization statements.) However, it's easy to create subtle errors by initializing a data member with the wrong constructor argument.

For example, you may define the following class:

class InitPick
{ int data1;
  int data2;
  int data3;

 public:
  InitPick(int a, int b, int c) :
    data1(a), data3(b), data2(c)
  {}
};

If you look closely, you may notice that we've initialized the member data3 with argument b. We probably meant to initialize the data3 member using argument c.

Since, in this example, we chose to give the data members and constructor arguments such obviously ordered names, spotting the error isn't that difficult. In most cases, though, you'll probably use more meaningful data member names.

Accordingly, you'll want to find similarly useful names for identifying the purpose of the constructor arguments. What you may not know is that you can use constructor argument names that are identical to the data member names, as long as you use those names in the initializer list only.

To illustrate, it's perfectly legal to rewrite the constructor above as

InitPick(int data1, int data2, int data3) :
  data1(data1), data2(data2), data3(data3)
{}

This naming convention works because of some special scoping rules for constructors.

Normally, a member function's arguments exist at function scope, meaning that those names will take priority over (or hide) the class's data members that use the same names from the point you name them in the argument list until the end of the function body (the closing brace). If you try to use the names of data members for a function's argument names, you won't be able to access the data members in the function body without using explicit scoping syntax such as

InitPick(int data1, int data2, int data3)
{
  InitPick::data1 = data1;
  InitPick::data2 = data2;
  InitPick::data3 = data3;
}

However, constructors contain a special scope within the initializer list that's possible due to the initialization syntax rules. Within the initialization list itself, the compiler implicitly adjusts the scope of the names that you're initializing (the names that appear prior to the parentheses). It does so because the names you initialize in this list must be data members of the class or base classes. Using anything else generates an error.

The comma operator

Most C++ programmers don't use the comma operator very often. However, it can be useful when you want to evaluate several expressions sequentially, but within the same statement.

Because the comma operator has the least precedence of all the C++ operators, you can be certain that the compiler will evaluate it last. However, some early C and C++ compilers didn't process the comma operator correctly. Recently, we came across the statement

delete x, y, z

The programmer who wrote this code intended to destroy all three variables. However, the code above will delete object x only.

As we mentioned earlier, the compiler will always evaluate the comma operator last in any expression. Accordingly, in the statement above, the compiler destroys x, evaluates both y and z (a process that does nothing), and then returns z. Beware the comma operator!

Return to the Borland C++ Developer's Journal index

Subscribe to the Borland C++ Developer's Journal


Copyright (c) 1996 The Cobb Group, a division of Ziff-Davis Publishing Company. All rights reserved. Reproduction in whole or in part in any form or medium without express written permission of Ziff-Davis Publishing Company is prohibited. The Cobb Group and The Cobb Group logo are trademarks of Ziff-Davis Publishing Company.